feat: layout snapshot support (beta) - #21
Merged
Merged
Conversation
Adds the twd-cli half of twd.matchLayout, which lands in twd-js 1.10.0.
matchLayout is off in the browser sidebar on purpose, because the sidebar
resizes the page and a developer's window is an arbitrary size, so twd-cli is
where a layout snapshot is actually decided.
Two flags, kept separate the way Jest separates them, because they close two
different holes:
--update-snapshots rewrites references that already exist
--ci forbids creating one, so a brand new test cannot write
its own baseline on the first CI run and pass forever
They are injected with evaluateOnNewDocument rather than evaluate, so they are
set before any script on the page runs and matchLayout sees them on first read.
BEHAVIOUR CHANGE: page.setViewport now runs on every run, not just when
recording. A normal run used to inherit Puppeteer's implicit size. This is
deliberate: layout snapshots are only reproducible if the viewport is fixed and
explicit, and relying on the implicit default would mean a Puppeteer upgrade
could change it and invalidate every committed reference at once, silently. The
default is 1280x800 and `viewport` in twd.config.json pins your own. While
recording, record.viewport still wins, so recording is unchanged.
A failure writes <name>.failed.png next to the reference, but in CI that machine
is gone by the time anyone looks. So the run also writes a self-contained
.twd/snapshot-report.html with every capture embedded as a data URI: one file,
one artifact, opens in any browser. Captures from earlier runs are cleared
first, since twd-js overwrites a capture on failure but never removes one when
the snapshot later passes, and a stale capture showing an already-fixed failure
is worse than no report.
Deliberately not built: a "3 snapshots written, 1 updated" summary line. It
would have guarded against leaving --update-snapshots on in a workflow, but
that is a user error and this is a beta nobody has run in anger yet. Dropping it
also means twd-cli needs nothing from twd-js beyond the window flags: the report
is built entirely from the PNGs on disk. See section 8 of the design doc.
TWD Contract Validation
23 passed · 41 failed · 3 warnings · 1 skipped Failed validations./contracts/users-3.0.json
./contracts/posts-3.1.json
./contracts/products-3.0.json
./contracts/events-3.1.json
|
This was referenced Sep 6, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The twd-cli half of
twd.matchLayout, which lands intwd-js1.10.0.matchLayoutis off in the browser sidebar on purpose: the sidebar resizes the page and a developer's window is an arbitrary size, so a reference created there fails for everyone else. twd-cli is where a layout snapshot is actually decided, and this is what it needs to hold up that end.Design:
docs/superpowers/specs/2026-09-06-layout-snapshots-design.md.Two flags, separate on purpose
Kept separate the way Jest separates them, because they close two different holes. Without
--ci, a brand new test writes its own baseline on the first CI run and passes forever, and nobody finds out. That is the expensive one, because it makes no noise.They go in via
evaluateOnNewDocument, notevaluate, so they are set before any script on the page runs andmatchLayoutsees them on first read. The precedence between them is decided intwd-js, which is the only side that has fetched the reference; reimplementing it here would be a second copy of a rule, free to drift.Behaviour change: the viewport
page.setViewport()now runs on every run, not just when recording. A normal run used to inherit Puppeteer's implicit size.This is deliberate. Layout snapshots are only reproducible if the viewport is fixed and explicit, and relying on the implicit default would mean a Puppeteer upgrade could change it and invalidate every committed reference at once, without a word. Default is
1280x800, andviewportintwd.config.jsonpins your own. While recording,record.viewportstill wins, so recording is untouched.A test that happened to depend on the old implicit size can behave differently. That is the one thing here that can affect someone who never uses snapshots.
Seeing the failure
A failure writes
<name>.failed.pngnext to the reference, but in CI that machine is gone by the time anyone looks. So the run also writes a self-contained.twd/snapshot-report.htmlwith every capture embedded as a data URI. One file, one artifact, opens in any browser, instead of a zip of loose PNGs to match up by filename.Captures from earlier runs are cleared first.
twd-jsoverwrites a capture on failure but never removes one when that snapshot later passes, so without the sweep a fixed layout keeps its old capture forever and the report shows a failure that no longer exists. That is worse than having no report.Deliberately not built
A
3 snapshots written, 1 updatedsummary line. It would have made it visible when--update-snapshotshad been left on in a workflow and had quietly rewritten every reference, but that is a user error and this is a beta nobody has run in anger yet. Building a guard against a failure mode we have not seen is guessing.Dropping it has a real payoff: twd-cli needs nothing from
twd-jsbeyond the window flags it already reads. The report is built entirely from the PNGs on disk. Failures still surface normally, sincematchLayoutthrows and the test fails with its message and a non-zero exit code.Also out of scope: merging snapshot results across shards, and any GitHub Actions job summary integration (it would put noise on pull requests).
Testing
479 tests passing, up from 466.
tests/snapshotReport.test.jsis new: 10 tests over real files in a tmpdir, covering the missing directory, no failures, two failures embedded, name derivation, an unreadable file skipped while the rest survive, an unwritable output directory, HTML escaping, and the stale sweep leaving.snapreferences alone.tests/parseArgs.test.jsandtests/config.test.jscover the flags and the two config keys, including that a partialviewportmerges over the default rather than wiping the height.tests/runTests.test.jspins the load-bearing detail: the injection happens beforegoto, asserted by invocation order, not by reading the code.